import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class FTPConnection extends Thread {

	final private static int FTP_PORT = 21;
	final private static Pattern generalPattern =  Pattern.compile("(...)[- ](.*)");
	final private static Pattern epsvReturnPattern = Pattern.compile(".* \\(\\|\\|\\|(.*)\\|\\)");
	final private static Pattern pwdReturnPattern = Pattern.compile("\"(.*)\" .*");

	final private static int POLL_PERIOD = 400;
	final private static int EPSV_RETRIES = 3;

	final private static String STARTING_TRANSFER = "150";
	final private static String SIZE_SUCCESS = 		"213";
	final private static String TRANSFER_SUCCESS =	"226";
	final private static String EPSV_SUCCESS = 		"229";
	final private static String USER_LOGGED_IN = 	"230";
	final private static String FILE_ACTION_OKAY =	"250";
	final private static String PASSWORD_NEEDED = 	"331";
	final private static String TIMEOUT=			"421";
	final private static String ALREADY_LOGGED_IN = "503";
	final private static String NOT_AUTHENTICATED = "530";

	private String username = "anonymous";
	private String password = "ftpclient@comp.nus.edu.sg";

	private FTPCallback callback;
	private String serverAddress = null;
	private Socket socket;
	private boolean connected = false;
	private boolean stopQueue = false;

	private BufferedReader in;
	private PrintWriter out;
	private LinkedList<FTPAction> actionQueue;

	final private FTPAction dummyAction = new FTPAction(){
		@Override
		void in(String returnCode, String s) {
			if(returnCode.equals(TIMEOUT)) {
				try {
					socket.close();
				} catch (IOException e) {}
				connected = false;
				callback.connected(false);
			}
		}
	};

	public FTPConnection(FTPCallback callback) {
		this.callback = callback;
		actionQueue = new LinkedList<FTPAction>();
		setDaemon(true);
	}

	private void setupConnection() throws UnknownHostException, IOException  {
		socket = new Socket(serverAddress,FTP_PORT);
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintWriter(socket.getOutputStream(),true);
		connected = true;
		queue(dummyAction);
		authenticate(username, password);

		callback.connected(true);
	}
	public void run() {
		try {
			setupConnection();
			while(connected){
				try {
					System.out.println("--------------------------Next Action-------------------------");
					FTPAction action = getNextAction();
					System.out.println("Got action");
					action.out();
					String input = null;
					String returnCode = null;
					do {
						input = in.readLine();
						if(input == null) throw new IOException("Connection cut.");
						System.out.println(input);
						callback.serverTextRecieved(input);
						if (input == null) break;
						Matcher m = generalPattern.matcher(input);
						if(m.matches()){
							returnCode = m.group(1);
							action.in(returnCode,m.group(2));
						}
					} while ( in.ready()
							||input.charAt(3) == '-'
								||returnCode.startsWith("1"));

					if(returnCode.equals(NOT_AUTHENTICATED)) {
						actionQueue.clear();
						callback.requiresAuthentication();
					}
				} catch (InterruptedException ine) {
					break;
				}
			}			
		} catch (IOException ie) {
			callback.connected(false);

		} finally {
			try {
				if(socket!=null) socket.close();
			} catch (IOException e) {}
			callback.connected(false);
		}
	}

	private synchronized FTPAction getNextAction() throws InterruptedException, IOException{
		do {
			if (!(actionQueue.isEmpty() || stopQueue)) {
				return actionQueue.pop();
			} else if (in.ready()) {
				return dummyAction;
			} else {
				wait(POLL_PERIOD);
			}
		}while(true);
	}
	private synchronized void queue(FTPAction action){
		actionQueue.addLast(action);
		notify();
	}
	private synchronized void push(FTPAction action){
		actionQueue.addFirst(action);
	}
	public synchronized void connect(String address,String username,String password){
		serverAddress = address;
		if(!(username == null || username.equals(""))) this.username = username;
		if(!(password == null || password.equals(""))) this.password = password;
		start();
	}


	/*
	 * Beginning of FTP methods.
	 */
	public void authenticate(final String user, final String pass) {
		final FTPAction actionPass =  new FTPAction() {
			boolean notified = false;
			void out() {
				OutgoingHelper.PASS.handle(out, pass);
			}
			void in(String returnCode,String s) {
				if(returnCode.equals(USER_LOGGED_IN)) {
					if(!notified) {
						callback.authenticationEnd(true);
						setBinaryMode(true);
						notified = true;
					}
				}
			}
		};
		final FTPAction actionUser =  new FTPAction() {
			boolean notified = false;
			void out() {
				OutgoingHelper.USER.handle(out, user);
			}
			void in(String returnCode,String s) {
				if(returnCode.equals(PASSWORD_NEEDED)) {
					push(actionPass);
				}
				else if(returnCode.equals(USER_LOGGED_IN) || returnCode.equals(ALREADY_LOGGED_IN)){
					if(!notified) {
						callback.authenticationEnd(true);
						setBinaryMode(true);
						notified = true;
					}
				}
				else {
					callback.authenticationEnd(false);
				}
			}
		};
		if(stopQueue) {
			push(actionUser);
		}
		else {
			queue(actionUser);
		}
	}

	public void list(){
		final EPSVAction epsvaction =  new EPSVAction();
		epsvaction.setFollowUp(new FTPAction() {
			void out(){
				OutgoingHelper.LIST.handle(out);
			}
			void in(String response, String s) {
				if(response.equals(STARTING_TRANSFER)) {
					callback.listStarted();
					try {
						epsvaction.dc.startReading();
					} catch (IOException e) {
						callback.listEnd(false);
					}
				}
				else if(response.equals(TRANSFER_SUCCESS)) {
					callback.listEnd(true);

				} else callback.listEnd(false);
			}
		});
		epsvaction.setDataReciever(new DataConnection.Reciever() {
			void lineRecieved(String line) {
				callback.listLineRecieved(line);
			}
		});
		queue(epsvaction);
	}

	public void cwd(final String directoryName){
		FTPAction action = new FTPAction() {
			void out() {
				OutgoingHelper.CWD.handle(out,directoryName);
			}
			void in(String returnCode, String s) {
				if(returnCode.equals(FILE_ACTION_OKAY)) callback.cwdEnd(true);
				else callback.cwdEnd(false);
			}
		};
		queue(action);
	}

	public void pwd() {
		FTPAction action = new FTPAction() {
			void out() {
				OutgoingHelper.PWD.handle(out);
			}
			void in(String returnCode, String s) {
				Matcher m = pwdReturnPattern.matcher(s);
				if(m.matches()) {
					callback.pwdEnd(m.group(1));
				}
				else callback.pwdEnd(null);
			}
		};
		queue(action);
	}

	public void size(final String filename) {
		final FTPAction sizeAction = new FTPAction() {
			void out() {
				OutgoingHelper.SIZE.handle(out, filename);
			}
			void in(String returnCode, String s) {
				if(returnCode.equals(SIZE_SUCCESS)) callback.sizeEnd(Integer.parseInt(s));
			}
		};
		queue(sizeAction);
	}

	public void retr(final String filename){
		try {
			final FileOutputStream fos =new FileOutputStream(new File(filename));
			final EPSVAction epsvaction =  new EPSVAction();
			epsvaction.setFollowUp(new FTPAction() {
				void out(){
					OutgoingHelper.RETR.handle(out, filename);
				}
				void in(String responseCode, String s)  {
					if(responseCode.equals(STARTING_TRANSFER)) {
						callback.retrStarted(filename);
						try {
							epsvaction.dc.startReading();
						} catch (IOException e) {
							callback.retrEnd(false);
						}
					}  else if (responseCode.equals(TRANSFER_SUCCESS)) {
						callback.retrEnd(true);
					}
					else callback.retrEnd(false);
				}
			});
			epsvaction.setDataReciever(new DataConnection.Reciever() {
				@Override
				void bytesRecieved(byte[] bytes, int byteCount) {
					try {
						fos.write(bytes,0,byteCount);
						callback.retrWriting(byteCount);
					} catch (IOException e) {
						callback.retrEnd(false);
					}
				}
				void closed() {
					try {
						fos.close();
						callback.retrEnd(true);
					} catch (IOException e) {}
				}
			});
			queue(epsvaction);
		} catch (IOException e) {}
	}

	public void stor(final File file) {
		try {
			final FileInputStream fis =new FileInputStream(file);
			final EPSVAction epsvaction =  new EPSVAction();
			epsvaction.setFollowUp(new FTPAction() {
				void out(){
					OutgoingHelper.STOR.handle(out,file.getName());
				}
				void in(String responseCode, String s){
					if(responseCode.equals(STARTING_TRANSFER)) {
						callback.storStarted(file.getName());
						try {
							epsvaction.dc.startWriting();
						} catch (IOException e) {
							callback.storEnd(false);
						}
					}
					else if(responseCode.equals(TRANSFER_SUCCESS)) {
						callback.storEnd(true);
					} else callback.storEnd(false);
				}
			});
			epsvaction.setDataSender(new DataConnection.Sender() {
				int sendBytes(byte[] bytes) {
					try {
						int byteCount = fis.read(bytes, 0, DataConnection.DATA_BLOCK_SIZE);
						callback.storWriting(byteCount);
						return byteCount;
					} catch (IOException e) {
						callback.storEnd(false);
						return -1;
					}
				}
				public void closed() {
					try {
						fis.close();
					} catch (IOException e) {
						callback.storEnd(false);
					}
				}
			});
			queue(epsvaction);
		} catch (IOException e) {}
	}

	public void setBinaryMode(final boolean bin) {
		FTPAction action = new FTPAction() {
			@Override
			void out() {
				if(bin)	OutgoingHelper.TYPE.handle(out,"I");
				else OutgoingHelper.TYPE.handle(out,"A");
			}
			@Override
			void in(String returnCode, String s) {
			}
		};
		queue(action);
	}

	public void disconnect() {
		FTPAction action =  new FTPAction() {
			@Override
			void out() {
				OutgoingHelper.QUIT.handle(out);
			}
			@Override
			void in(String returnCode, String s) {
				connected = false;
			}
		};
		queue(action);
	}

	/*
	 * End of FTP Methods
	 */


	/**
	 * @return the serverAddress
	 */
	public String getServerAddress() {
		return serverAddress;
	}
	/**
	 * @return the connected
	 */
	public boolean isConnected() {
		return connected;
	}	
	static abstract class FTPAction{
		void out(){}
		void in(String returnCode, String s){}
	}

	/**
	 * Wraps actions such that a data connection will be set up first.
	 * @author shawn
	 *
	 */
	class EPSVAction extends FTPAction{
		DataConnection dc;
		FTPAction followUp;
		DataConnection.Reciever dr;
		DataConnection.Sender ds;
		int tries = 1;
		void out(){
			OutgoingHelper.EPSV.handle(out);
		}
		void in(String returnCode,String s) {
			if(returnCode.equals(EPSV_SUCCESS)) {
				Matcher m = epsvReturnPattern.matcher(s);
				if(m.matches()){
					System.out.println(s);
					int port = Integer.parseInt(m.group(1));
					try {
						if(dr != null) {
							tries++;
							dc = new DataConnection(serverAddress,port,dr);
						}
						else if(ds != null) {
							tries++;
							dc = new DataConnection(serverAddress,port,ds);
						}
						push(followUp);
						callback.epsvEnd(true);
					}catch (IOException ioe) {
						if(tries < EPSV_RETRIES) push(this);
					}
				} else {
					callback.epsvEnd(false);
				}

			} else {
				callback.epsvEnd(false);
			}

		}

		void setFollowUp(FTPAction followUp) {
			this.followUp = followUp;
		}
		void setDataReciever(DataConnection.Reciever dr) {
			this.dr = dr;
		}
		void setDataSender(DataConnection.Sender ds) {
			this.ds = ds;
		}
	}
}
